package debtbundler.domain.bll.solver;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import debtbundler.domain.bll.Utils;
import debtbundler.domain.bll.solver.ClosestMatchSolver;
import debtbundler.domain.bll.solver.LinearSystemSolver;
import debtbundler.domain.bll.solver.SimpleSolver;
import debtbundler.domain.entity.Debt;
import debtbundler.domain.entity.Payment;
import debtbundler.domain.entity.SharedCost;
import debtbundler.domain.entity.Person;
import debtbundler.domain.entity.Share;
import debtbundler.domain.entity.impl.EntityManager;

public class SolverTest {
	
	LinearSystemSolver linearSystemSolver;
	
	@BeforeClass 
	public static void enableLogging() {
		debtbundler.logging.ConfigurationReader.setDefaultConf();
	}

	@Before
	public void setUp() throws Exception {
	}

	@After
	public void tearDown() throws Exception {
	}
	
	@Test
	public void testBalanceByEqualSharing() {
		Person anssi = EntityManager.createPerson();
		anssi.setName("Anssi");
		Person antti = EntityManager.createPerson();
		antti.setName("Antti");
		Person johannes = EntityManager.createPerson();
		johannes.setName("Johannes");
		Person tomi = EntityManager.createPerson();
		tomi.setName("Tomi");
		Person valtteri = EntityManager.createPerson();
		valtteri.setName("Valtteri");
		
		HashSet<Person> participants = new HashSet<Person>();
		participants.add(anssi);
		participants.add(antti);
		participants.add(johannes);
		participants.add(tomi);
		participants.add(valtteri);
		
		SharedCost safkat = EntityManager.createSharedCost();
		safkat.setName("Safkat");

		Payment anssinSafkaostos = EntityManager.createPayment(anssi, safkat);
		anssinSafkaostos.setValue(100);	
		Payment valtterinSafkaostos = EntityManager.createPayment(valtteri, safkat);
		valtterinSafkaostos.setValue(50);		
		
		Utils.balanceByEqualSharing(safkat, participants);
		
		for (Payment payment : safkat.getPayments()) {
			if (payment.getPerson().equals(anssi)) {
				assertEquals("Payments must be not be changed by equal sharing", 100, payment.getValue(), 0.0);
			} else if (payment.getPerson().equals(valtteri)) { 
				assertEquals("Payments must be not be changed by equal sharing", 50, payment.getValue(), 0.0);
			} else {
				fail();
			}
		}
		
		for (Share share : safkat.getShares()) {
			assertEquals("Costs shared equally", 30.0, share.getValue(), 0.0);
		}
		assertEquals("Balance of shared cost must be zero after equal sharing", 0, safkat.getBalance(), 0.0);
		
		assertEquals("Correct cost balance of person after sharing", -70.0, anssi.getCostBalance(), 0.0);
		assertEquals("Correct cost balance of person after sharing", -20.0, valtteri.getCostBalance(), 0.0);
		assertEquals("Correct cost balance of person after sharing", 30.0, antti.getCostBalance(), 0.0);
		assertEquals("Correct cost balance of person after sharing", 30.0, johannes.getCostBalance(), 0.0);
		assertEquals("Correct cost balance of person after sharing", 30.0, tomi.getCostBalance(), 0.0);
	}
	
	/**
	 * A very simple test.
	 */
	@Test
	public void oneDebtorOneCreditorSimpleSolver() throws Exception {
		Person creditor = EntityManager.createPerson();
		Payment payment = EntityManager.createPayment(creditor, EntityManager.createSharedCost());
		payment.setValue(37);
		
		Person debtor = EntityManager.createPerson();
		Share share = EntityManager.createShare(debtor, EntityManager.createSharedCost());
		share.setValue(37);
		
		Set<Person> input = new HashSet<Person>();
		input.add(creditor);
		input.add(debtor);
		
		SimpleSolver ss = new SimpleSolver(input);
		assertEquals(1, ss.getDebts().size());
		Debt debt = ss.getDebts().iterator().next();
		assertEquals(debtor, debt.getDebtor());
		assertEquals(creditor, debt.getCreditor());
		assertEquals(37, debt.getValue(), 0);
		assertEquals(0, debtor.getBalance(), 0);
		assertEquals(0, creditor.getBalance(), 0);
		
	}

	/**
	 * A very simple test.
	 */
	@Test
	public void oneDebtorOneCreditorLinSysSolver() throws Exception {
		Person creditor = EntityManager.createPerson();
		Payment payment = EntityManager.createPayment(creditor, EntityManager.createSharedCost());
		payment.setValue(37);
		
		Person debtor = EntityManager.createPerson();
		Share share = EntityManager.createShare(debtor, EntityManager.createSharedCost());
		share.setValue(37);
		
		Set<Person> input = new HashSet<Person>();
		input.add(creditor);
		input.add(debtor);
		
		LinearSystemSolver lss = new LinearSystemSolver(input);
		assertEquals(1, lss.getDebts().size());
		Debt debt = lss.getDebts().iterator().next();
		assertEquals(debtor, debt.getDebtor());
		assertEquals(creditor, debt.getCreditor());
		assertEquals(37, debt.getValue(), 0);
		assertEquals(0, debtor.getBalance(), 0);
		assertEquals(0, creditor.getBalance(), 0);
		
	}	
	
	/**
	 * This case is a test for a simple case where there is one 
	 * person who has paid for the costs that shared equally 
	 * by everyone.
	 * 
	 * @throws Exception
	 */
	@Test
	public void testOneDebtorManyCreditors() throws Exception {
						
		Person anssi = EntityManager.createPerson();
		anssi.setName("Anssi");
		Person antti = EntityManager.createPerson();
		antti.setName("Antti");
		Person johannes = EntityManager.createPerson();
		johannes.setName("Johannes");
		Person tomi = EntityManager.createPerson();
		tomi.setName("Tomi");
		Person valtteri = EntityManager.createPerson();
		valtteri.setName("Valtteri");
		
		SharedCost safkat = EntityManager.createSharedCost();
		safkat.setName("Safkat");
		SharedCost juomat = EntityManager.createSharedCost();
		juomat.setName("Juomat");
		

		// Anssi is the only one who has paid for the purchases, costs are shared equally  
		Payment anssinSafkaostos = EntityManager.createPayment(anssi, safkat);
		anssinSafkaostos.setValue(100);
		Payment anssinJuomaostos = EntityManager.createPayment(anssi, juomat);
		anssinJuomaostos.setValue(50);
		
		HashSet<Person> participants = new HashSet<Person>();
		participants.add(anssi);
		participants.add(antti);
		participants.add(johannes);
		participants.add(tomi);
		participants.add(valtteri);
		
		Utils.balanceByEqualSharing(safkat, participants);
		Utils.balanceByEqualSharing(juomat, participants);
		
		LinearSystemSolver solver = new LinearSystemSolver(participants);
		List<Debt> debts = solver.getDebts();
		
		// Assert the values of the debts: everyone else owes Anssi a value of 30.00.
		for (Debt debt : debts) {			
			if (debt.getCreditor() != anssi && !(debt.getDebtor().equals(antti) || debt.getDebtor().equals(johannes) || debt.getDebtor() == tomi || debt.getDebtor().equals(valtteri)) ) {
				fail("Discovered an unexpected debt");
			} else {
				assertEquals("Everyone else owes Anssi a value of 30", 30.0, debt.getValue(), 0.001);
			}
		}
		
	}
	
	/**
	 * This case is a more complex debtbundler case where there are more than one 
	 * debtor and more than creditor.
	 * 
	 * @throws Exception
	 */
	@Test	
	public void testManyDebtorsManyCreditors() throws Exception {
		
		Person anssi = EntityManager.createPerson();
		anssi.setName("Anssi");
		Person antti = EntityManager.createPerson();
		antti.setName("Antti");
		Person johannes = EntityManager.createPerson();
		johannes.setName("Johannes");
		Person tomi = EntityManager.createPerson();
		tomi.setName("Tomi");
		Person valtteri = EntityManager.createPerson();
		valtteri.setName("Valtteri");
		
		HashSet<Person> participants = new HashSet<Person>();
		participants.add(anssi);
		participants.add(antti);
		participants.add(johannes);
		participants.add(tomi);
		participants.add(valtteri);
		
		SharedCost safkat = EntityManager.createSharedCost();
		safkat.setName("Safkat");
		SharedCost juomat = EntityManager.createSharedCost();
		juomat.setName("Juomat");
		SharedCost bensat = EntityManager.createSharedCost();
		bensat.setName("Bensat");
		SharedCost pokeri = EntityManager.createSharedCost();
		pokeri.setName("Pokeri");
		
		Payment payment;
		payment = EntityManager.createPayment(anssi, safkat);
		payment.setValue(100);
		payment = EntityManager.createPayment(johannes, safkat);
		payment.setValue(30);
		Utils.balanceByEqualSharing(safkat, participants);
		
		payment = EntityManager.createPayment(tomi, juomat);
		payment.setValue(70);
		payment = EntityManager.createPayment(antti, juomat);
		payment.setValue(30);
		Utils.balanceByEqualSharing(juomat, participants);
		
		payment = EntityManager.createPayment(valtteri, bensat);
		payment.setValue(50);
		Utils.balanceByEqualSharing(bensat, participants);
		
		
		// Poker game with a stack of 40 EUR per person. 
		// Tomi wins 120 EUR, Antti wins 80 EUR. The rest get nothing.
		// The unpaid winnings become costs to the winners.
		Share share = EntityManager.createShare(tomi, pokeri);
		share.setValue(40);
		share = EntityManager.createShare(antti, pokeri);
		share.setValue(40);
		share = EntityManager.createShare(anssi, pokeri);
		share.setValue(40);
		share = EntityManager.createShare(johannes, pokeri);
		share.setValue(40);
		share = EntityManager.createShare(valtteri, pokeri);
		share.setValue(40);
		payment = EntityManager.createPayment(tomi, pokeri);
		payment.setValue(120);
		payment = EntityManager.createPayment(antti, pokeri);
		payment.setValue(80);

		LinearSystemSolver solver = new LinearSystemSolver(participants);
		//SimpleSolver solver = new SimpleSolver(participants);
		
		for (Person person : participants) {
			assertEquals("Balance of " + person.getName() + " must be zero", 0, person.getBalance(), 0.005);
		}
		
		for (Debt debt : solver.getDebts()) {
			System.out.println(debt);
		}
	}
	
	
	/**
	 * In this case the minimum number of debts is six or less. SimpleSolver finds 7 debts if
	 * it always matches greatest debts against greatest credits. ClosestMatchSolver and 
	 * LinearSystemSolver both find six debts. 
	 * <p>
	 * Note that solvers do not care if individual items are balanced or not. All that
	 * matters is that the payment balances of the persons sum to zero.
	 */
	@Test
	public void testMinimizingDebts1() {
		Set<Person> input = new HashSet<Person>();
		
		Person person = EntityManager.createPerson();
		input.add(person);
		Payment payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(21);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(63);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(28);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(241);
		
		person = EntityManager.createPerson();
		input.add(person);
		Share share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(28);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(31);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(280);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(14);

		SimpleSolver ss = new SimpleSolver(input);
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		for (Debt debt : ss.getDebts()) {
			System.out.println(debt);
		}
		
		System.out.println();
		
		ClosestMatchSolver cms = new ClosestMatchSolver(input);
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		for (Debt debt : cms.getDebts()) {
			System.out.println(debt);
		}
		
		System.out.println();
		
		LinearSystemSolver lss = new LinearSystemSolver(input);
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		for (Debt debt : lss.getDebts()) {
			System.out.println(debt);
		}
		
	}

	/**
	 * In this case the minimum number of debts is six or less. ClosestMatchSolver finds 7 debts,
	 * LinearSystemSolver finds six debts. 
	 */
	@Test
	public void testMinimizingDebts2() {
		Set<Person> input = new HashSet<Person>();
		
		Person person = EntityManager.createPerson();
		input.add(person);
		Payment payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(145);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(427);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(182);
		
		person = EntityManager.createPerson();
		input.add(person);
		payment = EntityManager.createPayment(person, EntityManager.createSharedCost());
		payment.setValue(107);
		
		person = EntityManager.createPerson();
		input.add(person);
		Share share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(580);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(157);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(99);
		
		person = EntityManager.createPerson();
		input.add(person);
		share = EntityManager.createShare(person, EntityManager.createSharedCost());
		share.setValue(25);
		
		
		SimpleSolver ss = new SimpleSolver(input);
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		System.out.println("Simple solver:");
		for (Debt debt : ss.getDebts()) {
			System.out.println(debt);
		}
		
		System.out.println();
		
		ClosestMatchSolver cms = new ClosestMatchSolver(input);
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		System.out.println("Closest match solver:");
		for (Debt debt : cms.getDebts()) {
			System.out.println(debt);
		}
		
		System.out.println();
		
		LinearSystemSolver lss = new LinearSystemSolver(input);
		System.out.println("Linear system solver:");
		for (Person p : input) {
			assertEquals("Balance of " + p.getName() + " must be zero", 0, p.getBalance(), 0.005);
		}
		for (Debt debt : lss.getDebts()) {
			System.out.println(debt);
		}
		
	}	
	
	/**
	 * Tests two different solvers with randomized input data.
	 */
	public void randomizedTesting() {
		
		Random generator = new Random();
		final int maxPersons = 5;
		while (true) {
			Set<Person> debtors = new HashSet<Person>();
			Set<Person> creditors = new HashSet<Person>();
			
			double totalCredit = Math.round(generator.nextDouble()*1000.0);
			double totalDebt = totalCredit;
			
			while (totalCredit > 0 && creditors.size() < maxPersons) {
				double creditShare;
				if (creditors.size() < maxPersons-1) { 
					creditShare =  Math.round(generator.nextDouble()*totalCredit);
				} else {
					creditShare = totalCredit;
				}
				Person creditor = EntityManager.createPerson();
				Payment payment = EntityManager.createPayment(creditor, EntityManager.createSharedCost());
				payment.setValue(creditShare);
				creditors.add(creditor);
				totalCredit -= creditShare;
			}
			
			while (totalDebt > 0 && debtors.size() < maxPersons) {
				double debtShare;
				if (debtors.size() < maxPersons-1) { 
					debtShare = Math.round(generator.nextDouble()*totalDebt);
				} else {
					debtShare = totalDebt;
				}
				Person debtor = EntityManager.createPerson();
				Share share = EntityManager.createShare(debtor, EntityManager.createSharedCost());
				share.setValue(debtShare);
				debtors.add(debtor);
				totalDebt -= debtShare;
			}
			
			Set<Person> input = new HashSet<Person>();
			input.addAll(debtors);
			input.addAll(creditors);
			
			System.out.println("Solving system with " + debtors.size() + " debtors, " + creditors.size() + " creditors...");
			
			try {
				long st = System.currentTimeMillis();
				LinearSystemSolver linSysSolver = new LinearSystemSolver(input);
				long et = System.currentTimeMillis();
				System.out.println("Linear system solution took " + (et-st) + " ms");
				
				st = System.currentTimeMillis();
				ClosestMatchSolver cms = new ClosestMatchSolver(input);
				et = System.currentTimeMillis();
				System.out.println("Closest match solution took " + (et-st) + " ms");
				
				if (linSysSolver.getDebts().size() == cms.getDebts().size()) {
					System.out.println("Both solvers reported " + linSysSolver.getDebts().size() + " debts");
				} else {
					System.err.println("Solvers reported different number of debts!");
					System.err.println("Linear system solution: " + linSysSolver.getDebts().size());
					System.err.println("Closest match: " + cms.getDebts().size());
					for (Person debtor : debtors) {
						System.err.println("Debtor: " + Utils.sum(debtor.getPayments()));
					}
					for (Person creditor : creditors) {
						System.err.println("Creditor: " + Utils.sum(creditor.getShares()));
					}
					//return;
				}
			} catch (Exception e) {
				e.printStackTrace();
				for (Person debtor : debtors) {
					System.err.println("Debtor: " + debtor.getBalance());
				}
				for (Person creditor : creditors) {
					System.err.println("Creditor: " + creditor.getBalance());
				}
				//return;
			}
			
		}
		
	}
	
	public static void main(String[] args) {
		SolverTest test = new SolverTest();
		test.randomizedTesting();
	}
}
